/*
 * Copyright (c) 2010, University of Innsbruck, Austria.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 * You should have received a copy of the GNU Lesser General Public License along
 * with this library; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

package at.sti2.sepn.pattern.network;

import java.util.ArrayList;
import java.util.List;

import org.apache.log4j.Logger;

import at.sti2.sepn.core.triple.RDFTriple;
import at.sti2.sepn.core.triple.RDFVariable;
import at.sti2.sepn.pattern.condition.TripleCondition;
import at.sti2.sepn.pattern.condition.TripleConstantTest;
import at.sti2.sepn.pattern.condition.TriplePatternGraph;
import at.sti2.sepn.pattern.network.alpha.AlphaMemory;
import at.sti2.sepn.pattern.network.alpha.AlphaNode;
import at.sti2.sepn.pattern.network.alpha.ConstantTestAlphaNode;
import at.sti2.sepn.pattern.network.alpha.WorkingMemory;
import at.sti2.sepn.pattern.network.beta.BetaMemory;
import at.sti2.sepn.pattern.network.beta.JoinNode;
import at.sti2.sepn.pattern.network.beta.JoinNodeTest;
import at.sti2.sepn.pattern.network.beta.NegativeNode;
import at.sti2.sepn.pattern.network.beta.ProductionNode;
import at.sti2.sepn.pattern.network.node.RETENode;

public class RETENetwork {

	private WorkingMemory workingMemory = null;
	private BetaMemory betaMemory = null;
	
	static Logger logger = Logger.getLogger(RETENetwork.class);
	
	public RETENetwork(){
		
		//Initialize alpha memory
		workingMemory = new WorkingMemory();
		
		//Initialize beta memory
		betaMemory = new BetaMemory(); //This is dummy top node
		betaMemory.setRootNode();
		
	}
	
	public RETENode buildOrShareNegativeNode(RETENode parentNode, AlphaMemory alphaMemory, List <JoinNodeTest> tests){
		
		for (RETENode childNode : parentNode.getChildren())
			if ((childNode instanceof NegativeNode)&&
			   (((NegativeNode)childNode).getAlphaMemory() == alphaMemory)&&
			   (((NegativeNode)childNode).getTests() == tests)) //TODO This is not a right way to compare lists
				return childNode;
		
		NegativeNode negativeNode = new NegativeNode();
		parentNode.getChildren().add(negativeNode);
		negativeNode.setParent(parentNode);
		negativeNode.setAlphaMemory(alphaMemory);
		negativeNode.setTests(tests);
		
		//TODO Insert head at the list of am.successors
		alphaMemory.getSuccessors().add(negativeNode);
		
		betaMemory.update();
		
		return negativeNode;
		
	}
	
	public RETENode buildOrShareBetaMemoryNode(RETENode parentNode){
		
		for (RETENode childNode : parentNode.getChildren())
			if (childNode instanceof BetaMemory) //TODO Do they think here about comparing types or values
				return childNode;
		
		BetaMemory betaMemory = new BetaMemory();
		betaMemory.setParent(parentNode);
		//TODO Insert new at the head of the list parent.children
		parentNode.getChildren().add(betaMemory);
		betaMemory.update();
		
		return betaMemory;
	}
	
	public RETENode buildOrShareJoinNode(RETENode parentNode, AlphaMemory alphaMemory, List <JoinNodeTest> tests){
		
		for (RETENode childNode : parentNode.getChildren())
			if ((childNode instanceof JoinNode)&&
			   (((JoinNode)childNode).getAlphaMemory() == alphaMemory)&&
			   (((JoinNode)childNode).getTests() == tests)) //TODO This is not a right way to compare lists
				return childNode;
		
		JoinNode joinNode = new JoinNode();
		joinNode.setParent(parentNode);
		
		//TODO Insert new at the head of the list parent.children
		parentNode.getChildren().add(joinNode);
		joinNode.setAlphaMemory(alphaMemory);
		joinNode.setTests(tests);
		
		//TODO Insert head at the list of am.successors
		alphaMemory.getSuccessors().add(joinNode);
		
		return joinNode;
	}
	
	public List <JoinNodeTest> getTestsFromCondition(TripleCondition condition, List <TripleCondition> previousConditions){
		
		List <JoinNodeTest> tests = new ArrayList <JoinNodeTest> ();
		
		//Test whether variable occurs at subject
		if (condition.isVariableAtSubject()){
			
			TripleCondition earlierCondition =  variableOccurs(
					(RDFVariable)condition.getConditionTriple().getSubject(),
					previousConditions);
			
			if (earlierCondition != null){
				
				JoinNodeTest test = new JoinNodeTest(
						RDFTriple.Field.SUBJECT,
						getVariableField((RDFVariable)condition.getConditionTriple().getSubject(), earlierCondition),
						previousConditions.indexOf(earlierCondition));
				
				tests.add(test);
			}
		}
		
		//Test whether variable occurs at predicate
		if (condition.isVariableAtPredicate()){
			
			TripleCondition earlierCondition =  variableOccurs(
					(RDFVariable)condition.getConditionTriple().getPredicate(),
					previousConditions);
			
			if (earlierCondition != null){
				
				JoinNodeTest test = new JoinNodeTest(
						RDFTriple.Field.PREDICATE,
						getVariableField((RDFVariable)condition.getConditionTriple().getPredicate(), earlierCondition),
						previousConditions.indexOf(earlierCondition));
				
				tests.add(test);
			}
		}
		
		//Test whether variable occurs at object
		if (condition.isVariableAtObject()){
			
			TripleCondition earlierCondition =  variableOccurs(
					(RDFVariable)condition.getConditionTriple().getObject(),
					previousConditions);
			
			if (earlierCondition != null){
				
				JoinNodeTest test = new JoinNodeTest(
						RDFTriple.Field.PREDICATE,
						getVariableField((RDFVariable)condition.getConditionTriple().getObject(), earlierCondition),
						previousConditions.indexOf(earlierCondition));
				
				tests.add(test);
			}
		}
		
		return tests;
	}
	
	private AlphaMemory buildOrShareAlphaMemory(TripleCondition condition){
		
		AlphaMemory alphaMemory = null;
		
		//Top node of alpha memory network
		ConstantTestAlphaNode currentNode = (ConstantTestAlphaNode)workingMemory.getRootNode();
		
		for (TripleConstantTest constantTest : condition.getConstantTests()){
			
			currentNode = buildOrShareConstantTestNode(currentNode, constantTest.getTestField(), constantTest.getLexicalTestSymbol());
			
			if (currentNode.getOutputMemory() != null)
				return currentNode.getOutputMemory();
			
			alphaMemory = new AlphaMemory();
			currentNode.setOutputMemory(alphaMemory);
			
			//Initialize alphaMemory with all the current WMEs
			//TODO Initialize the working memory with existing wmes
			//for (WorkingMemoryElement wme : workingMemory.getWorkingMemoryElements())
		
		}
		
		return alphaMemory;
	}
	
	private ConstantTestAlphaNode buildOrShareConstantTestNode(ConstantTestAlphaNode constantTestAlphaNode, RDFTriple.Field field, String lexicalTestSymbol){
		
		ConstantTestAlphaNode returnNode = null;
		
		for (AlphaNode childNode : constantTestAlphaNode.getChildren()){
			if (((ConstantTestAlphaNode)childNode).getFildToTest().equals(field)&&
				((ConstantTestAlphaNode)childNode).getLexicalTestValue().equals(lexicalTestSymbol))
				return (ConstantTestAlphaNode)childNode;
		}
		
		ConstantTestAlphaNode.FildToTest fieldToTest = null;
		if (field.equals(RDFTriple.Field.SUBJECT))
			fieldToTest = ConstantTestAlphaNode.FildToTest.SUBJECT;
		else if (field.equals(RDFTriple.Field.PREDICATE))
			fieldToTest = ConstantTestAlphaNode.FildToTest.PREDICATE;
		else if (field.equals(RDFTriple.Field.OBJECT))
			fieldToTest = ConstantTestAlphaNode.FildToTest.OBJECT;
		
		returnNode = new ConstantTestAlphaNode(fieldToTest, lexicalTestSymbol);
		constantTestAlphaNode.addChild(returnNode);
		
		return returnNode;
	}
	
	private TripleCondition variableOccurs(RDFVariable variable, List <TripleCondition> conditions){
		
		TripleCondition variableOccursTripleCondition = null;
		
		//Assuming that the conditions are ordered in the way they are ordered in the rule
		//we are going backwards so to find the latest occurrence of the variable
		TripleCondition condition = null;
		
		for (int i = conditions.size() - 1; i>=0; i--){
			
			condition = conditions.get(i);
			
			if (condition.getConditionTriple().getSubject() instanceof RDFVariable)
				if (variable.equals((RDFVariable)condition.getConditionTriple().getSubject())){
					variableOccursTripleCondition = condition;
					break;
				}
			
			if (condition.getConditionTriple().getPredicate() instanceof RDFVariable)
				if (variable.equals((RDFVariable)condition.getConditionTriple().getPredicate())){
					variableOccursTripleCondition = condition;
					break;
				}
			
			if (condition.getConditionTriple().getObject() instanceof RDFVariable)
				if (variable.equals((RDFVariable)condition.getConditionTriple().getObject())){
					variableOccursTripleCondition = condition;
					break;
				}
		}
		
		return variableOccursTripleCondition;
	}
	
	private RDFTriple.Field getVariableField(RDFVariable variable, TripleCondition condition){
		
		RDFTriple.Field field = null;
		
		if (condition.getConditionTriple().getSubject() instanceof RDFVariable)
			if (variable.equals((RDFVariable)condition.getConditionTriple().getSubject())){
				field = RDFTriple.Field.SUBJECT;
			}
		
		if (condition.getConditionTriple().getPredicate() instanceof RDFVariable)
			if (variable.equals((RDFVariable)condition.getConditionTriple().getPredicate())){
				field = RDFTriple.Field.PREDICATE;
			}
		
		if (condition.getConditionTriple().getObject() instanceof RDFVariable)
			if (variable.equals((RDFVariable)condition.getConditionTriple().getObject())){
				field = RDFTriple.Field.OBJECT;
			}
	
		return field;
	}
	
	public void addTriplePatternGraph(TriplePatternGraph triplePatternGraph){
		
		RETENode currentNode = betaMemory;
		
		List <TripleCondition> previousConditions = new ArrayList <TripleCondition> ();
		List <TripleCondition> tripleConditions = triplePatternGraph.getConditions();
		
		//TODO Check whether there is anything to add, i.e., whether the list is empty or not
		
		List <JoinNodeTest> tests = getTestsFromCondition(tripleConditions.get(0), previousConditions);
		AlphaMemory alphaMemory = buildOrShareAlphaMemory(tripleConditions.get(0));
		currentNode =  buildOrShareJoinNode(currentNode, alphaMemory, tests);
		
		for (int i = 1; i < tripleConditions.size(); i++){
			
			if (tripleConditions.get(i).isPositive()){
				currentNode = buildOrShareBetaMemoryNode(currentNode);
				previousConditions.add(tripleConditions.get(i-1));
				tests = getTestsFromCondition(tripleConditions.get(i), previousConditions);
				alphaMemory = buildOrShareAlphaMemory(tripleConditions.get(i));
				currentNode = buildOrShareJoinNode(currentNode, alphaMemory, tests);
			} else {
				tests = getTestsFromCondition(tripleConditions.get(i), previousConditions);
				alphaMemory = buildOrShareAlphaMemory(tripleConditions.get(i));
				currentNode = buildOrShareNegativeNode(currentNode, alphaMemory, tests);
			}
		}
		
		//Build a new production node, make it a child of current node
		ProductionNode productionNode = new ProductionNode();
		currentNode.addChild(productionNode);
		productionNode.setParent(currentNode);
		
		//Update the new node with the matches above
		productionNode.update();
		
	}
	
	public void addProduction(List <TripleCondition> conditions){
		
		RETENode currentNode = null;
		
		currentNode = buildOrShareNetworkForConditions(betaMemory, conditions, new ArrayList <TripleCondition> ());
		
		//TODO The problem with this one is in the issue of building first beta node while not marking it root
		//Build a new production node, make it a child of current node
		ProductionNode productionNode = new ProductionNode();
		currentNode.addChild(productionNode);
		productionNode.setParent(currentNode);
		
		//Update the new node with the matches above
		productionNode.update();
		
	}
	
	public RETENode buildOrShareNetworkForConditions(RETENode parent, List <TripleCondition> conditions, List <TripleCondition> earlierConditions){
		
		RETENode currentNode = parent;
		List <TripleCondition> conditionsHigherUp = earlierConditions;
		List <JoinNodeTest> tests = null;
		AlphaMemory alphaMemory = null;
		
		for (TripleCondition condition : conditions){
			
			if (condition.isPositive()){
				currentNode = buildOrShareBetaMemoryNode(currentNode);
				tests = getTestsFromCondition(condition, conditionsHigherUp);
				alphaMemory = buildOrShareAlphaMemory(condition);
				currentNode = buildOrShareJoinNode(currentNode, alphaMemory, tests);
			} else {
				tests = getTestsFromCondition(condition, conditionsHigherUp);
				alphaMemory = buildOrShareAlphaMemory(condition);
				currentNode = buildOrShareNegativeNode(currentNode, alphaMemory, tests);
			}
			
			conditionsHigherUp.add(condition);
		}
		
		return currentNode;
	}
	
	public void printNetworkStructure(){

		//Logging out the network structure
		logger.debug("================= NETWORK STRUCTURE - START ================= ");
		
		logger.debug("******************* ALPHA Memory Structure *******************");
		
		printAlphaNode(workingMemory.getRootNode(), "  ");
		
		logger.debug("******************* ALPHA Memory Structure *******************");
		
		logger.debug("******************* BETA Memory Structure *******************");
		
		printBetaNode(betaMemory, "  ");
		
		logger.debug("******************* BETA Memory Structure *******************");
		
		logger.debug("================= NETWORK STRUCTURE - END   ================= ");
		
	}
	
	public void printAlphaNode(AlphaNode alphaNode, String prefix){
		
		logger.debug(prefix + "Field to test:" + ((ConstantTestAlphaNode)alphaNode).getFildToTest());
		logger.debug(prefix + "Value to test:" + ((ConstantTestAlphaNode)alphaNode).getLexicalTestValue());
		if (alphaNode.getOutputMemory() != null)
			logger.debug(prefix + "Alpha mem id:" + Integer.toHexString(alphaNode.getOutputMemory().hashCode()));
		logger.debug(prefix + "---");
		
		for (AlphaNode childNode : alphaNode.getChildren())
			printAlphaNode(childNode, prefix + "  ");
	}
	
	public void printBetaNode(RETENode reteNode, String prefix){
		
		StringBuffer buffer = new StringBuffer();
		
		buffer.append(prefix);
		buffer.append("Type: ");
		if (reteNode instanceof JoinNode){
			buffer.append("JoinNode\n");
			buffer.append(prefix);
			buffer.append("Alpha mem id:" + Integer.toHexString((((JoinNode)reteNode).getAlphaMemory()).hashCode()) + '\n');
			for (JoinNodeTest test : ((JoinNode)reteNode).getTests()){
				buffer.append(prefix);
				buffer.append("[ARG1Field]:" + test.getArg1Field() + " [ARG2Field]:" + test.getArg2Field() + " [ARGCONDTNUM]:" + test.getArg2ConditionNumber() + '\n');
			}
			
		} else if (reteNode instanceof BetaMemory){
			if (((BetaMemory)reteNode).isRootNode())
				buffer.append("RootNode");
			else
				buffer.append("BetaMemory");
		} else if (reteNode instanceof ProductionNode) {
			buffer.append("ProductionNode");
	    } else if (reteNode instanceof NegativeNode) {
	    	buffer.append("NegativeNode\n");
			buffer.append(prefix);
			buffer.append("Alpha mem id:" + Integer.toHexString((((NegativeNode)reteNode).getAlphaMemory()).hashCode()) + '\n');
			for (JoinNodeTest test : ((NegativeNode)reteNode).getTests()){
				buffer.append(prefix);
				buffer.append("[ARG1Field]:" + test.getArg1Field() + " [ARG2Field]:" + test.getArg2Field() + " [ARGCONDTNUM]:" + test.getArg2ConditionNumber() + '\n');
			}
        }
		
		logger.debug(buffer.toString());
		logger.debug(prefix + "---");
		
		for (RETENode childNode : reteNode.getChildren())
			printBetaNode(childNode, prefix + "  ");
	}

	public WorkingMemory getWorkingMemory() {
		return workingMemory;
	}

	public BetaMemory getBetaMemory() {
		return betaMemory;
	}
	
}
